﻿using System;
using System.Diagnostics;
using Game.FlowField.Burst;
using Game.FlowField.Components;
using Game.FlowField.Input;
using Gmae.FlowField.Burst;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Entities.UniversalDelegates;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using UnityEngine.UIElements;
using Debug = UnityEngine.Debug;

namespace Game.FlowField.Systems
{
    [BurstCompile]
    [UpdateInGroup(typeof(SimulationSystemGroup), OrderLast = true)]
    public partial class MainSystem : SystemBase
    {
        private Config m_Config;

        private MapData m_MapData;
        private int m_BatchCount;
        private bool m_JobHandleCreated;
        private JobHandle m_JobHandle;
        private FlexibleArray<Unit> m_Units;
        private FlexibleArray<int> m_UnitGridIndexTos;
        private float m_Speed;
        private int m_CurrentFrameCount;
        private int2 UnitCenterIndex2D;

        [BurstDiscard]
        protected override void OnCreate()
        {
            base.OnCreate();
            this.m_BatchCount = Math.Max(SystemInfo.processorCount - 1, 1);
        }

        protected override void OnStartRunning()
        {
            base.OnStartRunning();

            this.m_Config = SystemAPI.GetSingleton<Config>();
            this.m_Units = new FlexibleArray<Unit>(1024, Unity.Collections.Allocator.Persistent);
            this.m_UnitGridIndexTos = new FlexibleArray<int>(1024, Unity.Collections.Allocator.Persistent);
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
        }

        [BurstCompile]
        protected override void OnUpdate()
        {
            if(this.m_JobHandleCreated == true)
            {
                this.m_JobHandleCreated = false;
                this.m_JobHandle.Complete();
            }
            
            this.m_Speed = FlowField.MovingSpeed;
            this.m_CurrentFrameCount = UnityEngine.Time.frameCount;
            this.UpdateUnits();

            if (Operations.TryDequeue(out var operation))
            {
                switch (operation)
                {
                    case RebuildMapOperation rebuildMapOperation:
                        RebuildMap(rebuildMapOperation);
                        break;
                    case AddUnitOperation addUnitOperation:
                        if (TryAddUnit(addUnitOperation, out var addingUnitCount))
                            FlowField.UnitIncrease(addingUnitCount);
                        break;
                    case RemoveUnitOperation removeUnitOperation:
                        if (TryRemoveUnit(removeUnitOperation, out var removingUnitCount))
                            FlowField.UnitDecrease(removingUnitCount);
                        break;
                    case AddBlockOperation addBlockOperation:
                        if (TryAddBlock(addBlockOperation, out var addingBlockCount))
                        {
                            FlowField.BlockIncrease(addingBlockCount);
                            this.m_JobHandle = this.m_MapData.ScheduleUpdateWeight();
                            this.m_JobHandleCreated = true;
                        }
                        break;
                    case RemoveBlockOperation removeBlockOperation:
                        if (TryRemoveBlock(removeBlockOperation, out var removingBlockCount))
                        {
                            FlowField.BlockDecrease(removingBlockCount);
                            this.m_JobHandle = this.m_MapData.ScheduleUpdateWeight();
                            this.m_JobHandleCreated = true;
                        }
                        break;
                    case ResetTargetOperation resetTargetOperation:
                        if (this.TryResetTarget(resetTargetOperation, out var targetInfo))
                        {
                            this.m_JobHandle = this.m_MapData.ScheduleUpdateWeight();
                            this.m_JobHandleCreated = true;
                            ControlPanel.Singleton.UpdateTarget(targetInfo);
                        }
                        break;
                }

                operation.Cache();
            }
        }

        private void UpdateUnits()
        {
            if(!this.m_MapData.TargetInfo.Active)
                return;

            var unitCenterIndex2D = new NativeArray<int2>(1, Allocator.TempJob);
            unitCenterIndex2D[0] = this.UnitCenterIndex2D;
            var unitMovingJob = new UnitMovingJob();
            unitMovingJob.WrapData(
                this.m_MapData, 
                this.m_Units.Array, 
                FlowField.UnitCount,
                unitCenterIndex2D,
                SystemAPI.Time.DeltaTime * this.m_Speed);
            unitMovingJob.Schedule().Complete();
            this.UnitCenterIndex2D = unitCenterIndex2D[0];

            var unitCount = this.m_Units.Length;
            var unitArray = this.m_Units.Array;
            Entities.WithAll<UnitComponentData>().ForEach((ref TransformAspect transform, in UnitComponentData unitComponentData) =>
            {
                var index = unitComponentData.UnitIndex;
                if (index >= 0 && index < unitCount)
                {

                    var unit = unitArray[index];
                    transform.Position = new float3(unit.Position, 0f);
                }
            }).Run();
        }

        private bool TryResetTarget(ResetTargetOperation resetTargetOperation, out TargetInfo targetInfo) => this.TryResetTarget(resetTargetOperation.Index, resetTargetOperation.Index2D, out targetInfo);

        private bool TryResetTarget(int index, int2 index2D, out TargetInfo targetInfo)
        {
            if (MapDataUtil.IsInsideMap(index2D, m_MapData.Size))
            {
                if (!this.m_MapData.GridIsBlocks[index])
                {
                    this.m_MapData.TargetInfo = new TargetInfo()
                    {
                        Active = true,
                        Index = index,
                        Index2D = index2D,
                        Position = this.m_MapData.GridPositions[index]
                    };
                    targetInfo = this.m_MapData.TargetInfo;

                    return true;
                }
            }

            targetInfo = default;
            return false;
        }

        private bool TryAddUnit(AddUnitOperation addUnitOperation, out int addingCount)
        {
            var coverPoints = new NativeArray<int2>((addUnitOperation.BrushSize + 1) * (addUnitOperation.BrushSize + 1), Allocator.Temp);
            var brushHelper = new BrushHelper(addUnitOperation.BrushSize, addUnitOperation.Index2D);
            brushHelper.CalculateCoverIndex2D(coverPoints);

            var positions = new NativeArray<float2>(coverPoints.Length, Allocator.TempJob);
            var unitIndexFrom = this.m_Units.Length;

            var mapSize = this.m_MapData.Size;
            addingCount = 0;
            for (int i = 0; i < coverPoints.Length; i++)
            {
                var coverPoint = coverPoints[i];
                if (MapDataUtil.IsInsideMap(coverPoint, mapSize))
                {
                    var index = MapDataUtil.ConvertIndex2D2Index(coverPoint, mapSize);
                    if (TryAddUnit(index, coverPoint))
                    {
                        positions[addingCount] = this.m_MapData.GridPositions[index];
                        addingCount++;
                    }
                }
            }

            if(addingCount > 0)
            {
                var newEntities = new NativeArray<Entity>(addingCount, Allocator.Temp);
                var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
                var ecb = ecbSingleton.CreateCommandBuffer(this.CheckedStateRef.WorldUnmanaged);
                ecb.Instantiate(this.m_Config.UnitEntity, newEntities);
                
                for (int i = 0; i < addingCount; i++)
                {
                    var unitEntity = newEntities[i];
                    ecb.SetComponent(unitEntity, new UnitComponentData() { UnitIndex = unitIndexFrom + i });
                    ecb.SetComponent(unitEntity, new Unity.Transforms.LocalToWorldTransform()
                    {
                        Value = new Unity.Transforms.UniformScaleTransform()
                        {
                            Position = new Unity.Mathematics.float3(positions[i].xy, 0f),
                            Rotation = Unity.Mathematics.quaternion.identity,
                            Scale = 1f
                        }
                    });
                }
            }

            return addingCount > 0;
        }
        private bool TryAddUnit(int index, int2 index2D)
        {
            if (MapDataUtil.IsInsideMap(index2D, m_MapData.Size))
            {
                if (!this.m_MapData.GridIsBlocks[index] &&
                    this.m_MapData.GridOccupies[index] == 0)
                {
                    this.m_MapData.GridOccupies[index] += 1;
                    var position = this.m_MapData.GridPositions[index];

                    var unit = new Unit();
                    unit.ResetAsNewOne();
                    unit.Setup(index, position);
                    this.m_Units.Add(unit);
                    this.m_UnitGridIndexTos.Add(default);

                    return true;
                }
            }

            return false;
        }

        private bool TryRemoveUnit(RemoveUnitOperation removeUnitOperation, out int removingCount)
        {
            var coverPoints = new NativeArray<int2>((removeUnitOperation.BrushSize + 1) * (removeUnitOperation.BrushSize + 1), Allocator.Temp);
            var brushHelper = new BrushHelper(removeUnitOperation.BrushSize, removeUnitOperation.Index2D);
            brushHelper.CalculateCoverIndex2D(coverPoints);

            var removingIndexArray = new NativeArray<int>(coverPoints.Length, Allocator.Temp);

            var mapSize = this.m_MapData.Size;
            var coverGridIndexArray = new NativeArray<int>(coverPoints.Length, Allocator.Temp);
            var insideMapCoverCount = 0;
            removingCount = 0;
            for (int i = 0; i < coverPoints.Length; i++)
            {
                var coverPoint = coverPoints[i];
                if (MapDataUtil.IsInsideMap(coverPoint, mapSize))
                {
                    var index = MapDataUtil.ConvertIndex2D2Index(coverPoint, mapSize);
                    coverGridIndexArray[insideMapCoverCount] = index;
                    insideMapCoverCount++;
                }
            }

            var insideMapCoverGridIndexArray = new NativeArray<int>(insideMapCoverCount, Allocator.TempJob);
            NativeArray<int>.Copy(coverGridIndexArray, 0, insideMapCoverGridIndexArray, 0, insideMapCoverCount);

            var copyJob = new CopyGridIndexToJob() 
            { 
                GridIndexToArray = this.m_UnitGridIndexTos.Array, 
                UnitArray = this.m_Units.Array 
            };
            copyJob.ScheduleBatch(this.m_Units.Length, math.max(this.m_Units.Length / this.m_BatchCount, 1)).Complete();

            var coverUnitIndexArray = new NativeArray<int>(insideMapCoverGridIndexArray.Length, Allocator.TempJob);
            var findUnitIndexJob = new FindUnitIndexJob() 
            { 
                CoverGridIndexArray = insideMapCoverGridIndexArray,
                CoverUnitIndexArray = coverUnitIndexArray,
                TotalUnitGridIndexToArray = this.m_UnitGridIndexTos.Array,
                UnitCount = this.m_Units.Length
            };
            findUnitIndexJob.ScheduleBatch(coverUnitIndexArray.Length, math.max(coverUnitIndexArray.Length / this.m_BatchCount, 1)).Complete();

            for(int i = 0; i < coverUnitIndexArray.Length; i++)
            {
                if (TryRemoveUnit(coverUnitIndexArray[i], out var removingUnitIndex))
                {
                    removingIndexArray[removingCount] = removingUnitIndex;
                    removingCount++;
                }
            }

            if (removingCount > 0)
            {
                var finalRemovingIndexArray = new NativeArray<int>(removingCount, Allocator.TempJob);
                NativeArray<int>.Copy(removingIndexArray, 0, finalRemovingIndexArray, 0, removingCount);
                var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
                var ecb = ecbSingleton.CreateCommandBuffer(this.CheckedStateRef.WorldUnmanaged);
                var removeBlockEntityJob = new RemoveUnitEntityJob() { RemovingIndexArray = finalRemovingIndexArray, ECB = ecb.AsParallelWriter() };
                removeBlockEntityJob.ScheduleParallel();
            }
            
            return removingCount > 0;
        }
        private bool TryRemoveUnit(int unitIndex, out int removingUnitIndex)
        {
            removingUnitIndex = -1;

            if(unitIndex >=0 && unitIndex < this.m_Units.Length)
            {
                var unit = this.m_Units.Array[unitIndex];
                var unitArray = this.m_Units.Array;
                if (unit.IsAlive)
                {
                    unit.ResetAsDeadOne();
                    unitArray[unitIndex] = unit;

                    this.m_MapData.GridOccupies[unit.GridIndexTo] -= 1;
                    removingUnitIndex = unitIndex;

                    return true;
                }
            }

            return false;
        }

        private void RebuildMap(RebuildMapOperation operation)
        {
            this.m_MapData = new MapData(operation.Width, operation.Height, 0.64f);

            var mapInitJob = new MapDataInitJob();
            mapInitJob.WrapData(this.m_MapData);
            mapInitJob.ScheduleBatch(this.m_MapData.GridCount, math.max(this.m_MapData.GridCount / m_BatchCount, 1)).Complete();
        }

        private bool TryAddBlock(AddBlockOperation addBlockOperation, out int addingCount)
        {
            var coverPoints = new NativeArray<int2>((addBlockOperation.BrushSize + 1) * (addBlockOperation.BrushSize + 1), Allocator.Temp);
            var brushHelper = new BrushHelper(addBlockOperation.BrushSize, addBlockOperation.Index2D);
            brushHelper.CalculateCoverIndex2D(coverPoints);

            var addingIndexArray = new NativeArray<int>(coverPoints.Length, Allocator.Temp);
            var mapSize = this.m_MapData.Size;
            addingCount = 0;
            for(int i = 0; i < coverPoints.Length; i++)
            {
                var coverPoint = coverPoints[i];
                if (MapDataUtil.IsInsideMap(coverPoint, mapSize))
                {
                    var index = MapDataUtil.ConvertIndex2D2Index(coverPoint, mapSize);
                    if (TryAddBlock(index, coverPoint))
                    {
                        addingIndexArray[addingCount] = index;
                        addingCount++;
                    }
                }
            }

            if(addingCount > 0)
            {
                var finalAddingIndexArray = new NativeArray<int>(addingCount, Allocator.Temp);
                NativeArray<int>.Copy(addingIndexArray, 0, finalAddingIndexArray, 0, addingCount);

                var newEntityArray = new NativeArray<Entity>(addingCount, Allocator.Temp);
                var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
                var ecb = ecbSingleton.CreateCommandBuffer(this.CheckedStateRef.WorldUnmanaged);
                ecb.Instantiate(this.m_Config.BlockEntity, newEntityArray);
                for (int i = 0; i < finalAddingIndexArray.Length; i++)
                {
                    var index = finalAddingIndexArray[i];
                    var position = this.m_MapData.GridPositions[index];

                    var blockEntity = newEntityArray[i];

                    ecb.SetComponent(blockEntity, new MapBlock() { GridIndex = index });
                    ecb.SetComponent(blockEntity, new Unity.Transforms.LocalToWorldTransform()
                    {
                        Value = new Unity.Transforms.UniformScaleTransform()
                        {
                            Position = new Unity.Mathematics.float3(position.xy, 0f),
                            Rotation = Unity.Mathematics.quaternion.identity,
                            Scale = 1f
                        }
                    });
                }
            }

            return addingCount > 0;
        }
        private bool TryAddBlock(int index, int2 index2D)
        {
            if (MapDataUtil.IsInsideMap(index2D, m_MapData.Size))
            {
                if (!this.m_MapData.GridIsBlocks[index] &&
                    this.m_MapData.GridOccupies[index] == 0 &&
                    this.m_MapData.TargetInfo.Index != index)
                {
                    this.m_MapData.GridIsBlocks[index] = true;

                    return true;
                }
            }

            return false;
        }

        private bool TryRemoveBlock(RemoveBlockOperation removeBlockOperation, out int removingCount)
        {
            var coverPoints = new NativeArray<int2>((removeBlockOperation.BrushSize + 1) * (removeBlockOperation.BrushSize + 1), Allocator.Temp);
            var brushHelper = new BrushHelper(removeBlockOperation.BrushSize, removeBlockOperation.Index2D);
            brushHelper.CalculateCoverIndex2D(coverPoints);

            var removeIndexArray = new NativeArray<int>(coverPoints.Length, Allocator.Temp);
            var mapSize = this.m_MapData.Size;
            removingCount = 0;
            for (int i = 0; i < coverPoints.Length; i++)
            {
                var coverPoint = coverPoints[i];
                if (MapDataUtil.IsInsideMap(coverPoint, mapSize))
                {
                    var index = MapDataUtil.ConvertIndex2D2Index(coverPoint, mapSize);
                    if (TryRemoveBlock(index, coverPoint))
                    {
                        removeIndexArray[removingCount] = index;
                        removingCount++;
                    }
                }
            }

            if(removingCount > 0)
            {
                var finalRemoveIndexArray = new NativeArray<int>(removingCount, Allocator.TempJob);
                NativeArray<int>.Copy(removeIndexArray, 0, finalRemoveIndexArray, 0, removingCount);
                var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
                var ecb = ecbSingleton.CreateCommandBuffer(this.CheckedStateRef.WorldUnmanaged);
                var removeBlockEntityJob = new RemoveBlockEntityJob()
                { 
                     RemovingIndexArray = finalRemoveIndexArray,
                     ECB = ecb.AsParallelWriter()
                };
                removeBlockEntityJob.ScheduleParallel();
            }

            return removingCount > 0;
        }
        private bool TryRemoveBlock(int index, int2 index2D)
        {
            if (MapDataUtil.IsInsideMap(index2D, m_MapData.Size))
            {
                var gridIndex = index;
                if (this.m_MapData.GridIsBlocks[gridIndex])
                {
                    this.m_MapData.GridIsBlocks[gridIndex] = false;
                    return true;
                }
            }

            return false;
        }
    }
}